package service;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import service.cities.City;
import service.cities.CityByIp;
import service.cities.CityByName;
import service.cities.CityByCoordinates;
import service.functions.Function;
import service.functions.FunctionValue;
import service.functions.FunctionService;
import service.states.StateService;
import service.states.StateValue;
import service.xml.ServiceResponse;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created with IntelliJ IDEA.
 * User: Claudio "Dna" Bonesana
 * Date: 14.04.13
 * Time: 17:42
 */

public class ServiceStatus {
    /*
        Functions list:
            setCityByName
            setCityByIp
            setCityByCoordinates
            removeCityByName
            removeCityByIp
            removeCityByCoordinates
            setUpdateTime
            setremoteKey
     */
    private Map<String, Function> functions = new HashMap<String, Function>();
    private Map<String,City> cities = new HashMap<String,City>();

    private int updateTiming;
    private String serverName;
    private String key;
    private String port;

    public ServiceStatus() throws StatusException {

        System.out.println(System.getProperty("user.dir") + "/start.xml");

        // XML stream reader
        InputStream is;
        try {
            is = new FileInputStream(System.getProperty("user.dir") + "/start.xml");
        } catch (FileNotFoundException e) {
            is = ServiceStatus.class.getResourceAsStream("../start.xml");
        }

        try {

            // Parse the configuration XML to retrive configuration info and available functions
            Function temp;
            String name;

            DocumentBuilderFactory docBuilderFact = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docBuilderFact.newDocumentBuilder();

            // New DOM document
            Document doc = docBuilder.parse(is);

            serverName = doc.getElementsByTagName("Server").item(0).getTextContent();
            if (serverName.equals("")){
                throw new StatusException("Undefined server insertionName.");
            }
            key = doc.getElementsByTagName("RemoteKey").item(0).getTextContent();
            if (key.equals("")){
                throw new StatusException("Undefined key.");
            }
            port = doc.getElementsByTagName("Port").item(0).getTextContent();
            if (port.equals("")){
                throw new StatusException("Undefined port.");
            }

            NodeList functionList = doc.getElementsByTagName("Function");

            for (int i=0; i<functionList.getLength(); i++){

                Node nNode = functionList.item(i);

                if (nNode.getNodeType() == Node.ELEMENT_NODE) {

                    Element element = (Element) nNode;

                    name = element.getElementsByTagName("Name").item(0).getTextContent();

                    temp = new Function(name);

                    NodeList parameters = element.getElementsByTagName("Parameter");

                    for (int j=0; j<parameters.getLength(); j++){

                        temp.addParams(parameters.item(j).getTextContent());

                    }

                    functions.put(name,temp);
                }

            }

        } catch (ParserConfigurationException e) {
            e.printStackTrace();
            throw new StatusException(e);
        } catch (SAXException e) {
            e.printStackTrace();
            throw new StatusException(e);
        } catch (IOException e) {
            e.printStackTrace();
            throw new StatusException(e);
        }
    }

    public Map<String,City> getCities() {
        return cities;
    }

    public int getUpdateTiming() {
        return updateTiming;
    }

    public void setUpdateTiming(int updateTiming) {
        this.updateTiming = updateTiming;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public synchronized void removeCity(String name){
        cities.remove(name);
    }

    /**
     * Execute function addCityByName.
     * @param value the city name
     * @throws StatusException
     */
    public synchronized void addCityByName(FunctionValue value) throws StatusException {
        if (value.getPlace().equals(""))
            throw new StatusException("Undefined Place in addCityByName.");
        City city = new CityByName(value.getPlace());
        city.setComment(value.getComment());
        cities.put(value.getPlace(), city);
    }

    /**
     * Execute function addCityByIp.
     * @param value a valid Ip.
     * @throws StatusException
     */
    public synchronized void addCityByIp(FunctionValue value) throws StatusException {
        if (value.getIp().equals(""))
            throw new StatusException("Undefined Ip in addCityByIp.");
        if (!validate(value.getIp()))
            throw new StatusException("Malformed or invalid ip.");
        City city = new CityByIp(value.getIp());
        city.setComment(value.getComment());
        cities.put(value.getIp(), city);
    }

    /**
     * Execute function addCityByCoordinate
     * @param value a valid latitude and longitude
     * @throws StatusException
     */
    public synchronized void addCityByCoordinate(FunctionValue value) throws StatusException {
        if (value.getLongitude().equals(""))
            throw new StatusException("Undefined Longitude in addCityByCoordinates.");
        if (value.getLatitude().equals(""))
            throw new StatusException("Undefined latitude in addCityByCoordinates.");
        City city = new CityByCoordinates(value.getLatitude(), value.getLongitude());
        city.setComment(value.getComment());
        cities.put(value.getLatitude() + ";" + value.getLongitude(), city);
    }

    /**
     * Wrapper
     * @param doc a DOM document
     * @param tag a tag to search
     * @return the current document data
     */
    private synchronized String getDocumentData(Document doc, String tag){
        String str = doc + " " + tag;
        return doc.getElementsByTagName(tag).item(0).getTextContent();
    }

    /**
     * Update the current status with information from the World Weather Online service.
     * This method executed by the server on a precise schedule.
     * @throws ParserConfigurationException
     * @throws SAXException
     */
    public synchronized void updateData() throws ParserConfigurationException, SAXException {
        String url;
        String str;

        // for each available city
        for (City city : cities.values()){

            // Avoid Weather Online limit of 3 queries every seconds
            try {
                // Web service request
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // Nothing...
            }

            url = "http://api.worldweatheronline.com/free/v1/weather.ashx?" +
                    "q=" + city.getConnectionData() + "&" +
                    "format=xml&" +
                    "num_of_days=1&" +
                    "key=" + key;

            // Open a connection
            EasyConnection conn;
            try {
                conn = new EasyConnection(url);
                conn.open();
                conn.setMethod(EasyConnection.Method.GET);
                str = conn.getContent();
                conn.close();

                // XML stream reader
                StringReader sr = new StringReader(str);
                InputSource is = new InputSource(sr);

                DocumentBuilderFactory docBuilderFact = DocumentBuilderFactory.newInstance();
                DocumentBuilder docBuilder = docBuilderFact.newDocumentBuilder();

                // New DOM document
                Document doc = docBuilder.parse(is);

                // Updating local data
                String place = getDocumentData(doc, "query");
                if (!place.equals(city.getName())){
                    city.setName(place);
                }
                city.setObservationTime(getDocumentData(doc,"observation_time"));
                city.setTemperature(getDocumentData(doc, "temp_C"));
                city.setWeatherImg(getDocumentData(doc, "weatherIconUrl"));
                city.setWeatherDescr(getDocumentData(doc, "weatherDesc"));
                city.setWindStrength(getDocumentData(doc, "windspeedKmph"));
                city.setWindDirection(getDocumentData(doc,"winddir16Point"));
                city.setPrecipitation(getDocumentData(doc, "precipMM"));
                city.setHumidity(getDocumentData(doc, "humidity"));
                city.setPressure(getDocumentData(doc, "pressure"));
                System.out.println("UPDATED:  " + city.getConnectionData());
            } catch (IOException e) {
                System.out.println("ERROR:    updating " + city.getConnectionData());
                e.printStackTrace();
            }
        }
    }

    /**
     * DATA: request the current server status.
     * @return a ServiceResponse with the current server status
     */
    public synchronized ServiceResponse getData(){

        // Generating response XML
        ServiceResponse response = new ServiceResponse();
        ArrayList<StateService> ss = new ArrayList<StateService>();
        ArrayList<FunctionService> fs = new ArrayList<FunctionService>();

        FunctionService func;
        FunctionValue funcv;
        StateService stat;
        StateValue statv;

        StateValue keyValue = new StateValue();
        keyValue.setRemoteKey(key);
        StateService stateKey = new StateService();
        stateKey.setName("Key");
        stateKey.setValue(keyValue);

        StateValue secondsValue = new StateValue();
        secondsValue.setUpdateTiming(Integer.toString(updateTiming));
        StateService stateSeconds = new StateService();
        stateSeconds.setName("UpdateTime");
        stateSeconds.setValue(secondsValue);

        ss.add(stateKey);
        ss.add(stateSeconds);

        // Generating current status
        for (City city : cities.values()){
            statv = new StateValue();
            statv.setComment(city.getComment());
            statv.setTemperature(city.getTemperature());
            statv.setObservationTime(city.getObservationTime());
            statv.setWeatherImg(city.getWeatherImg());
            statv.setWeatherDescription(city.getWeatherDescr());
            statv.setWindStrength(city.getWindStrength());
            statv.setWindDirection(city.getWindDirection());
            statv.setPrecipitation(city.getPrecipitation());
            statv.setHumidity(city.getHumidity());
            statv.setPressure(city.getPressure());
            statv.setInsertionName(city.getInsertionName());

            stat = new StateService();
            stat.setName(city.getInsertionName());
            stat.setValue(statv);
            ss.add(stat);
        }
        response.setStates(ss);

        // Generating available functions
        for (Function function : functions.values()){

            funcv = new FunctionValue();

            for (String paramName : function.getParams()){
                if (paramName.equals("Comment")){
                    funcv.setComment(paramName);
                } else if (paramName.equals("Place")){
                    funcv.setPlace(paramName);
                } else if (paramName.equals("Ip")){
                    funcv.setIp(paramName);
                } else if (paramName.equals("Key")){
                    funcv.setKey(paramName);
                } else if (paramName.equals("Latitude")){
                    funcv.setLatitude(paramName);
                } else if (paramName.equals("Longitude")){
                    funcv.setLongitude(paramName);
                } else if (paramName.equals("Seconds")){
                    funcv.setSeconds(paramName);
                }
            }

            func = new FunctionService();
            func.setName(function.getName());
            func.setValue(funcv);

            fs.add(func);
        }
        response.setFunctions(fs);

        return response;
    }

    /**
     * SET: update the current status with information from clients.
     * @param functions functions sent by a client
     * @throws StatusException
     */
    public synchronized void updateStatus(ArrayList<FunctionService> functions) throws StatusException {

        // for each function
        for (FunctionService function : functions){
            FunctionValue value = function.getValue();
            String name = function.getName();

            // call the right method

            // set methods
            if (name.startsWith("setCity")){
                if (name.contains("ByName")){
                    addCityByName(value);
                    System.out.println("NEW CITY: " + value.getPlace());
                } else if (name.contains("ByIp")){
                    addCityByIp(value);
                    System.out.println("NEW CITY: " + value.getIp());
                } else if (name.contains("ByCoordinates")){
                    addCityByCoordinate(value);
                    System.out.println("NEW CITY: " + value.getLatitude() + " " + value.getLongitude());
                } else {
                    throw new StatusException("Unexpected add function: " + name);
                }
            // remove methods
            } else if (name.startsWith("removeCity")){
                if (name.contains("ByName")){
                    removeCity(value.getPlace());
                    System.out.println("REMOVED:  " + value.getPlace());
                } else if (name.contains("ByIp")){
                    removeCity(value.getIp());
                    System.out.println("REMOVED:  " + value.getIp());
                } else if (name.contains("ByCoordinates")){
                    removeCity(value.getLatitude() + ";" + value.getLongitude());
                    System.out.println("REMOVED:  " + value.getLatitude() + ";" + value.getLongitude());
                } else {
                    throw new StatusException("Unexpected remove function: " + name);
                }
            // other methods
            } else if (name.equals("setUpdateTime")){
                if (value.getSeconds().equals(""))
                    throw new StatusException("Undefined seconds in setUpdateTime.");
                this.updateTiming = Integer.getInteger(value.getSeconds());
                System.out.println("NEW TIME: every " + updateTiming + " seconds");
            } else if (name.equals("setRemoteKey")){
                if (value.getKey().equals(""))
                    throw new StatusException("Undefined key in setRemoteKey.");
                this.setKey(value.getKey());
                System.out.println("NEW KEY:  " + key);
            } else {
                throw new StatusException("Unexpected function: " + name);
            }
        }

    }

    public String getUri() {
        return "http://localhost:"+port+"/"+serverName+"/";
    }

    /**
     * Pattern used by validate method.
     */
    private static final String PATTERN = "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +  "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";

    /**
     * Check if an ip is valid or not.
     * @param ip a string representing an ip
     * @return
     */
    public boolean validate(final String ip){

        Pattern pattern = Pattern.compile(PATTERN);
        Matcher matcher = pattern.matcher(ip);
        return matcher.matches();
    }

}
